package org.codefinger.json.parser;

public class JSONLexer {

	private static final int	ERROR_RANGE	= 10;

	private static final char	END			= '\0';

	private char[]				input;

	private int					nextCharIndex;

	private JSONToken			tokenType;

	private int					tokenOffset;

	private int					tokenSize;

	private char				strBegin;

	public JSONLexer(char[] input) {
		this.input = input;
	}

	public JSONLexer(String input) {
		this.input = input.toCharArray();
	}

	public JSONToken token() {
		return tokenType;
	}

	public String tokenText() {
		return new String(input, tokenOffset, tokenSize);
	}

	public void skipCurrentString() {
		char ch;
		char begin = strBegin;
		int index = nextCharIndex;
		for (;;) {
			ch = charAt(index);
			switch (ch) {
			case '\'':
			case '"':
				if (begin == ch) {
					nextCharIndex = index + 1;
					return;
				}
				index++;
				continue;
			case '\\':
				if (charAt(++index) != END) {
					index++;
					continue;
				}
			case END:
				throw makeThrow();
			default:
				index++;
				continue;
			}
		}
	}

	public char[] getOriginalCharArray() {
		char ch;
		char begin = strBegin;
		int index = nextCharIndex, sOffset = index;
		for (;;) {
			ch = charAt(index);
			switch (ch) {
			case '\'':
			case '"':
				if (begin == ch) {
					nextCharIndex = index + 1;
					int length = index - sOffset;
					char[] result = new char[length];
					System.arraycopy(input, sOffset, result, 0, length);
					return result;
				}
				index++;
				continue;
			case '\\':
				if (charAt(++index) != END) {
					index++;
					continue;
				}
			case END:
				throw makeThrow();
			default:
				index++;
				continue;
			}
		}
	}

	public String getOriginalString() {
		return new String(getOriginalCharArray());
	}

	public String getJSONString() {
		char ch;
		char begin = strBegin;
		int index = nextCharIndex, sOffset = index;
		StringBuilder builder = new StringBuilder();
		BEGIN: for (;;) {
			ch = charAt(index);
			switch (ch) {
			case '\'':
			case '"':
				if (begin == ch) {
					nextCharIndex = index + 1;
					return builder.append(input, sOffset, index - sOffset).toString();
				}
				index++;
				continue;
			case '\\': {
				int length = index - sOffset;
				builder.append(input, sOffset, length);
				sOffset += length;
				switch (charAt(++index)) {
				case '"':
					builder.append('"');
					index++;
					sOffset += 2;
					continue;
				case '\'':
					builder.append('\'');
					index++;
					sOffset += 2;
					continue;
				case '\\':
					builder.append('\\');
					index++;
					sOffset += 2;
					continue;
				case '/':
					builder.append('/');
					index++;
					sOffset += 2;
					continue;
				case 'b':
					builder.append('\b');
					index++;
					sOffset += 2;
					continue;
				case 'f':
					builder.append('\f');
					index++;
					sOffset += 2;
					continue;
				case 'n':
					builder.append('\n');
					index++;
					sOffset += 2;
					continue;
				case 'r':
					builder.append('\r');
					index++;
					sOffset += 2;
					continue;
				case 't':
					builder.append('\t');
					index++;
					sOffset += 2;
					continue;
				case 'u': {
					int hax = 0;
					int digit;
					for (int i = 1; i < 5; i++) {
						digit = charAt(++index);
						if (digit > 47 && digit < 58) {
							hax = hax * 16 - digit + 48;
						} else if (digit > 96 && digit < 103) {
							hax = hax * 16 - digit + 87;
						} else if (digit > 64 && digit < 71) {
							hax = hax * 16 - digit + 55;
						} else {
							index++;
							continue BEGIN;
						}
					}
					builder.append((char) -hax);
					index++;
					sOffset += 6;
					continue;
				}
				default:
					sOffset += length;
					continue;
				}
			}
			case END:
				throw makeThrow();
			default:
				index++;
				continue;
			}
		}
	}

	public void nextToken() {
		int offset = nextCharIndex;
		char ch = nextChar();
		switch (ch) {
		case '{':
			setStatus(JSONToken.LEFT_CURLY, offset, 1);
			return;
		case '}':
			setStatus(JSONToken.RIGHT_CURLY, offset, 1);
			return;
		case '[':
			setStatus(JSONToken.LEFT_SQUARE, offset, 1);
			return;
		case ']':
			setStatus(JSONToken.RIGHT_SQUARE, offset, 1);
			return;
		case ',':
			setStatus(JSONToken.COMMA, offset, 1);
			return;
		case ':':
			setStatus(JSONToken.COLON, offset, 1);
			return;
		case ' ':
		case '\t':
		case '\r':
		case '\n': {
			// Skip the blank characters
			int index = nextCharIndex;
			for (;;) {
				switch (charAt(index)) {
				case ' ':
				case '\t':
				case '\r':
				case '\n':
					index++;
					continue;
				default:
					nextCharIndex = index;
					nextToken();
				}
				return;
			}
		}
		case END:
			setStatus(JSONToken.END, offset, 0);
			return;
		case 't':
		case 'T':
			if (((ch = nextChar()) == 'r' || ch == 'R') && ((ch = nextChar()) == 'u' || ch == 'U') && ((ch = nextChar()) == 'e' || ch == 'E')) {
				if (Character.isJavaIdentifierPart(charAt(nextCharIndex))) {
					nextCharIndex++;
					scanFieldName(offset);
					return;
				}
				setStatus(JSONToken.TRUE, offset, 4);
				return;
			}
			nextCharIndex--;
			scanFieldName(offset);
			return;
		case 'f':
		case 'F':
			if (((ch = nextChar()) == 'a' || ch == 'A') && ((ch = nextChar()) == 'l' || ch == 'L') && ((ch = nextChar()) == 's' || ch == 'S') && ((ch = nextChar()) == 'e' || ch == 'E')) {
				if (Character.isJavaIdentifierPart(charAt(nextCharIndex))) {
					nextCharIndex++;
					scanFieldName(offset);
					return;
				}
				setStatus(JSONToken.FALSE, offset, 5);
				return;
			}
			nextCharIndex--;
			scanFieldName(offset);
			return;
		case 'n':
		case 'N':
			if (((ch = nextChar()) == 'u' || ch == 'U') && ((ch = nextChar()) == 'l' || ch == 'L') && ((ch = nextChar()) == 'l' || ch == 'L')) {
				if (Character.isJavaIdentifierPart(charAt(nextCharIndex))) {
					nextCharIndex++;
					scanFieldName(offset);
					return;
				}
				setStatus(JSONToken.NULL, offset, 4);
				return;
			}
			nextCharIndex--;
			scanFieldName(offset);
			return;
		case 'u':
		case 'U':
			if (((ch = nextChar()) == 'n' || ch == 'N') && ((ch = nextChar()) == 'd' || ch == 'D') && ((ch = nextChar()) == 'e' || ch == 'E') && ((ch = nextChar()) == 'f' || ch == 'F') && ((ch = nextChar()) == 'i' || ch == 'I') && ((ch = nextChar()) == 'n' || ch == 'N') && ((ch = nextChar()) == 'e' || ch == 'E') && ((ch = nextChar()) == 'd' || ch == 'D')) {
				if (Character.isJavaIdentifierPart(charAt(nextCharIndex))) {
					nextCharIndex++;
					scanFieldName(offset);
					return;
				}
				setStatus(JSONToken.UNDIFINED, offset, 9);
				return;
			}
			nextCharIndex--;
			scanFieldName(offset);
			return;
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			scanNumber(offset, false);
			return;
		case '-':
			scanNumber(offset, true);
			return;
		case '\'':
		case '"': {
			strBegin = ch;
			setStatus(JSONToken.STRING, offset, 1);
			return;
		}
		default:
			if (Character.isJavaIdentifierPart(ch)) {
				scanFieldName(offset);
				return;
			}
			throw makeThrow();
		}
	}

	private void scanNumber(int offset, boolean isMunus) {
		int index = nextCharIndex;
		boolean pointFlag = false;
		boolean eFlag = false;
		for (;;) {
			switch (charAt(index)) {
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				index++;
				continue;
			case '.':
				if (!pointFlag && !eFlag && (!isMunus || index > nextCharIndex)) {
					pointFlag = true;
					index++;
					continue;
				}
				throw makeThrow();
			case 'e':
			case 'E':
				if (!eFlag && (!isMunus || index > nextCharIndex)) {
					eFlag = true;
					switch (charAt(++index)) {
					case '+':
					case '-':
						index++;
						continue;
					default:
						continue;
					}
				}
				throw makeThrow();
			default:
				nextCharIndex = index;
				setStatus(pointFlag || eFlag ? JSONToken.FLOAT : JSONToken.INT, offset, index - offset);
				return;
			}
		}
	}

	private void scanFieldName(int offset) {
		int index = nextCharIndex;
		for (;;) {
			if (Character.isJavaIdentifierPart(charAt(index))) {
				index++;
				continue;
			}
			setStatus(JSONToken.FIELD_NAME, offset, index - offset);
			nextCharIndex = index;
			return;
		}
	}

	private char charAt(int index) {
		char[] input = this.input;
		if (index >= input.length) {
			return END;
		}
		return input[index];
	}

	private char nextChar() {
		char[] input = this.input;
		if (nextCharIndex == input.length) {
			return END;
		}
		return input[nextCharIndex++];
	}

	private void setStatus(JSONToken token, int offset, int length) {
		this.tokenType = token;
		this.tokenOffset = offset;
		this.tokenSize = length;
	}

	public JSONSyntaxException makeThrow() {
		int errorRange = ERROR_RANGE;

		int offset = nextCharIndex - errorRange;
		if (offset < 0) {
			offset = 0;
		}

		int end = nextCharIndex + errorRange;
		if (end > input.length) {
			end = input.length;
		}

		throw new JSONSyntaxException(String.format("Matched a syntax error near by: %s", new String(input, offset, end - offset)));
	}

}
